﻿/* 
* Copyright (c) 2010, The University of Texas at Austin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, 
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, 
* this list of conditions and the following disclaimer in the documentation and/or other materials 
* provided with the distribution.
*
* Neither the name of The University of Texas at Austin nor the names of its contributors may be 
* used to endorse or promote products derived from this software without specific prior written 
* permission.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;

namespace Bio.Views.Structure.Editor.Internal.Rendering.Primitives
{
    public class HelixRectangle
    {
        public HelixRectangle(int fivePrimeStartNucleotide, int fivePrimeEndNucleotide,
            int threePrimeStartNucleotide, int threePrimeEndNucleotide, Dictionary<int, Point> allLocations)
        {
            FivePrimeStartNucleotide = fivePrimeStartNucleotide;
            FivePrimeEndNucleotide = fivePrimeEndNucleotide;
            ThreePrimeStartNucleotide = threePrimeStartNucleotide;
            ThreePrimeEndNucleotide = threePrimeEndNucleotide;
            Debug.Assert(allLocations.ContainsKey(FivePrimeStartNucleotide) &&
                         allLocations.ContainsKey(FivePrimeEndNucleotide) &&
                         allLocations.ContainsKey(ThreePrimeStartNucleotide) &&
                         allLocations.ContainsKey(ThreePrimeEndNucleotide));
            AllLocations = allLocations;
            UpdateProposedRectangle();
        }

        public void MoveNucleotideTo(int nucleotideIndex, Point location)
        {
            AllLocations[nucleotideIndex] = MoveNucleotideToInternal(nucleotideIndex, location);
            UpdateProposedRectangle();
        }

        public void MoveNucleotideBy(int nucleotideIndex, Point delta)
        {
            AllLocations[nucleotideIndex] = MoveNucleotideByInternal(nucleotideIndex, delta);
            UpdateProposedRectangle();
        }

        public void MoveBy(Point delta)
        {
            Dictionary<int, Point> newNtLocations = new Dictionary<int, Point>();
            foreach (var helixNt in AllLocations.Keys)
            {
                newNtLocations.Add(helixNt, MoveNucleotideByInternal(helixNt, delta));
            }
            AllLocations = newNtLocations;
            UpdateProposedRectangle();
        }

        //Uses the vector from the five prime opening base pair as a guide to snap the helix.
        public Dictionary<int, Point> SnapToOpeningBasePair(double bpPadding)
        {
            if (bpPadding <= 0)
                throw new ArgumentException("Error: Base Pair Padding must be Positive");
            
            Dictionary<int, Point> retValue = new Dictionary<int, Point>();
            int fivePrimeNt = FivePrimeStartNucleotide;
            int threePrimeNt = ThreePrimeEndNucleotide;

            while (fivePrimeNt < FivePrimeEndNucleotide)
            {
                StackBasePairsFromFivePrimeStart(fivePrimeNt, threePrimeNt, retValue, bpPadding);
                fivePrimeNt = fivePrimeNt + 1;
                threePrimeNt = threePrimeNt - 1;
            }
            UpdateProposedRectangle();
            return retValue;
        }

        public Dictionary<int, Point> SnapToClosingBasePair(double bpPadding)
        {
            if (bpPadding <= 0)
                throw new ArgumentException("Error: Base Pair Padding must be Positive");
            
            Dictionary<int, Point> retValue = new Dictionary<int, Point>();
            int fivePrimeNt = FivePrimeEndNucleotide;
            int threePrimeNt = ThreePrimeStartNucleotide;

            while (fivePrimeNt > FivePrimeStartNucleotide)
            {
                StackBasePairsFromFivePrimeEnd(fivePrimeNt, threePrimeNt, retValue, bpPadding);
                fivePrimeNt = fivePrimeNt - 1;
                threePrimeNt = threePrimeNt + 1;
            }
            UpdateProposedRectangle();
            return retValue;
        }

        public Point FivePrimeStartNucleotideLocation { get { return AllLocations[FivePrimeStartNucleotide]; } }
        public Point FivePrimeEndNucleotideLocation { get { return AllLocations[FivePrimeEndNucleotide]; } }
        public Point ThreePrimeStartNucleotideLocation { get { return AllLocations[ThreePrimeStartNucleotide]; } }
        public Point ThreePrimeEndNucleotideLocation { get { return AllLocations[ThreePrimeEndNucleotide]; } }
        public Dictionary<int, Point> AllLocations { get; private set; }
        
        //public Rect BoundingBox { get; private set; }
        public double RotationAngle { get; private set; }

        //private LocalNucleotideSequenceVector FivePrimeStrandVector { get; set; }
        private int FivePrimeStartNucleotide { get; set; }
        private int FivePrimeEndNucleotide { get; set; }
        private int ThreePrimeStartNucleotide { get; set; }
        private int ThreePrimeEndNucleotide { get; set; }

        private void UpdateProposedRectangle()
        {
            Point fivePrimeStartStdCoord = new Point(FivePrimeStartNucleotideLocation.X, -1 * FivePrimeStartNucleotideLocation.Y);
            Point fivePrimeEndStdCoord = new Point(FivePrimeEndNucleotideLocation.X, -1 * FivePrimeEndNucleotideLocation.Y);
            Point translationDelta = new Point(0 - fivePrimeStartStdCoord.X, 0 - fivePrimeStartStdCoord.Y);
            Point t_fivePrimeStartStdCoord = new Point(fivePrimeStartStdCoord.X + translationDelta.X, fivePrimeStartStdCoord.Y + translationDelta.Y);
            Point t_fivePrimeEndStdCoord = new Point(fivePrimeEndStdCoord.X + translationDelta.X, fivePrimeEndStdCoord.Y + translationDelta.Y);
            double proposedWidthStdCoord = new Vector(t_fivePrimeEndStdCoord.X, t_fivePrimeEndStdCoord.Y).Length;
            if (proposedWidthStdCoord > 0)
            {
                RotationAngle = Math.Acos(t_fivePrimeEndStdCoord.X / proposedWidthStdCoord);
            }
            else { RotationAngle = 0.0; }
        }

        private Point MoveNucleotideByInternal(int nucleotideIndex, Point delta)
        {
            if (!AllLocations.ContainsKey(nucleotideIndex))
                throw new ArgumentException(string.Format("Error: Nucleotide {0} is not within helix {1}", nucleotideIndex, FivePrimeStartNucleotide));
            return new Point(AllLocations[nucleotideIndex].X + delta.X, AllLocations[nucleotideIndex].Y + delta.Y);
        }

        private Point MoveNucleotideToInternal(int nucleotideIndex, Point location)
        {
            if (!AllLocations.ContainsKey(nucleotideIndex))
                throw new ArgumentException(string.Format("Error: Nucleotide {0} is not within helix {1}", nucleotideIndex, FivePrimeStartNucleotide));
            return location;
        }

        private void StackBasePairsFromFivePrimeEnd(int guideBpFivePrimeIdx, int guideBpThreePrimeIdx, Dictionary<int, Point> deltas, double bpPadding)
        {
            if (guideBpFivePrimeIdx <= FivePrimeStartNucleotide)
                return;

            Point fivePrimeStartStdCoord = new Point(AllLocations[guideBpFivePrimeIdx].X, -1 * AllLocations[guideBpFivePrimeIdx].Y);
            Point threePrimeEndStdCoord = new Point(AllLocations[guideBpThreePrimeIdx].X, -1 * AllLocations[guideBpThreePrimeIdx].Y);
            Point translationDelta = new Point(0 - fivePrimeStartStdCoord.X, 0 - fivePrimeStartStdCoord.Y);
            Point t_fivePrimeStartStdCoord = new Point(fivePrimeStartStdCoord.X + translationDelta.X, fivePrimeStartStdCoord.Y + translationDelta.Y); //This should now be the pseudo-origin for the math below.
            Point t_threePrimeEndStrCood = new Point(threePrimeEndStdCoord.X + translationDelta.X, threePrimeEndStdCoord.Y + translationDelta.Y);
            Vector bpVector = new Vector(t_threePrimeEndStrCood.X, t_threePrimeEndStrCood.Y);

            Point t_nextFivePrimeStdCoord = new Point(AllLocations[guideBpFivePrimeIdx - 1].X + translationDelta.X, (-1 * AllLocations[guideBpFivePrimeIdx - 1].Y) + translationDelta.Y);
            Point t_nextThreePrimeStdCoord = new Point(AllLocations[guideBpThreePrimeIdx + 1].X + translationDelta.X, (-1 * AllLocations[guideBpThreePrimeIdx + 1].Y) + translationDelta.Y);
            Vector perpendicularReference = new Vector();
            TargetQuadrant perpendicularQuadrant = TargetQuadrant.First;
            if (bpVector.X > 0 && bpVector.Y == 0)
            {
                //guide base pair is on the negative x axis
                perpendicularReference = new Vector(0, -1);
                perpendicularQuadrant = TargetQuadrant.Fourth;
            }
            else if (bpVector.X < 0 && bpVector.Y == 0)
            {
                //guide base pair is on the positive x axis
                perpendicularReference = new Vector(0, 1);
                perpendicularQuadrant = TargetQuadrant.Second;
            }
            else if (bpVector.Y > 0 && bpVector.X == 0)
            {
                perpendicularReference = new Vector(-1, 0);
                perpendicularQuadrant = TargetQuadrant.Third;
            }
            else if (bpVector.Y < 0 && bpVector.X == 0)
            {
                perpendicularReference = new Vector(1, 0);
                perpendicularQuadrant = TargetQuadrant.First;
            }
            else if (bpVector.X < 0 && bpVector.Y < 0)
            {
                perpendicularReference = new Vector(0, 1);
                perpendicularQuadrant = TargetQuadrant.Second;
            }
            else if (bpVector.X > 0 && bpVector.Y < 0)
            {
                perpendicularReference = new Vector(-1, 0);
                perpendicularQuadrant = TargetQuadrant.Third;
            }
            else if (bpVector.X > 0 && bpVector.Y > 0)
            {
                perpendicularReference = new Vector(0, -1);
                perpendicularQuadrant = TargetQuadrant.Fourth;
            }
            else if (bpVector.X < 0 && bpVector.Y > 0)
            {
                perpendicularReference = new Vector(1, 0);
                perpendicularQuadrant = TargetQuadrant.First;
            }
            else
            {
                //this is the case where the guide base pair vector has a magnitude of 0
            }

            if (Math.Abs(Vector.AngleBetween(bpVector, perpendicularReference)) == 90)
            {
                StackToNeighboringBasePair(bpPadding, bpVector, perpendicularReference, t_nextFivePrimeStdCoord, t_nextThreePrimeStdCoord, guideBpFivePrimeIdx - 1, guideBpThreePrimeIdx + 1, deltas);
            }
            else
            {
                Vector bpVectorPerpendicularUnitVector = CalculatePerpendicularUnitVector(bpVector, perpendicularQuadrant);
                StackToNeighboringBasePair(bpPadding, bpVector, bpVectorPerpendicularUnitVector, t_nextFivePrimeStdCoord, t_nextThreePrimeStdCoord, guideBpFivePrimeIdx - 1, guideBpThreePrimeIdx + 1, deltas);
            }
        }

        private void StackBasePairsFromFivePrimeStart(int guideBpFivePrimeIdx, int guideBpThreePrimeIdx, Dictionary<int, Point> deltas, double bpPadding)
        {
            if (guideBpFivePrimeIdx >= FivePrimeEndNucleotide)
                return;

            Point fivePrimeStartStdCoord = new Point(AllLocations[guideBpFivePrimeIdx].X, -1 * AllLocations[guideBpFivePrimeIdx].Y);
            Point threePrimeEndStdCoord = new Point(AllLocations[guideBpThreePrimeIdx].X, -1 * AllLocations[guideBpThreePrimeIdx].Y);
            Point translationDelta = new Point(0 - fivePrimeStartStdCoord.X, 0 - fivePrimeStartStdCoord.Y);
            Point t_fivePrimeStartStdCoord = new Point(fivePrimeStartStdCoord.X + translationDelta.X, fivePrimeStartStdCoord.Y + translationDelta.Y); //This should now be the pseudo-origin for the math below.
            Point t_threePrimeEndStrCood = new Point(threePrimeEndStdCoord.X + translationDelta.X, threePrimeEndStdCoord.Y + translationDelta.Y);
            Vector bpVector = new Vector(t_threePrimeEndStrCood.X, t_threePrimeEndStrCood.Y);

            Point t_nextFivePrimeStdCoord = new Point(AllLocations[guideBpFivePrimeIdx + 1].X + translationDelta.X, (-1 * AllLocations[guideBpFivePrimeIdx + 1].Y) + translationDelta.Y);
            Point t_nextThreePrimeStdCoord = new Point(AllLocations[guideBpThreePrimeIdx - 1].X + translationDelta.X, (-1 * AllLocations[guideBpThreePrimeIdx - 1].Y) + translationDelta.Y);
            Vector perpendicularReference = new Vector();
            TargetQuadrant perpendicularQuadrant = TargetQuadrant.First;
            if (bpVector.X > 0 && bpVector.Y == 0)
            {
                //guide base pair is on the positive x axis
                perpendicularReference = new Vector(0, 1);
                perpendicularQuadrant = TargetQuadrant.Second;
            }
            else if (bpVector.X < 0 && bpVector.Y == 0)
            {
                //guide base pair is on the negative x axis
                perpendicularReference = new Vector(0, -1);
                perpendicularQuadrant = TargetQuadrant.Fourth;
            }
            else if (bpVector.Y > 0 && bpVector.X == 0)
            {
                perpendicularReference = new Vector(-1, 0);
                perpendicularQuadrant = TargetQuadrant.Third;
            }
            else if (bpVector.Y < 0 && bpVector.X == 0)
            {
                perpendicularReference = new Vector(1, 0);
                perpendicularQuadrant = TargetQuadrant.First;
            }
            else if (bpVector.X < 0 && bpVector.Y < 0)
            {
                //guide base pair in third quadrant, perpendicular vertical negative or in fourth quadrant.
                perpendicularReference = new Vector(0, -1);
                perpendicularQuadrant = TargetQuadrant.Fourth;
            }
            else if (bpVector.X > 0 && bpVector.Y < 0)
            {
                //guide base pair in fourth quadrant, perpendicular horizontal positive or in first quadrant
                perpendicularReference = new Vector(1, 0);
                perpendicularQuadrant = TargetQuadrant.First;
            }
            else if (bpVector.X > 0 && bpVector.Y > 0)
            {
                //guide base pair in the first quadrant, perpendicular vertical positive or in second quadrant
                perpendicularReference = new Vector(0, 1);
                perpendicularQuadrant = TargetQuadrant.Second;
            }
            else if (bpVector.X < 0 && bpVector.Y > 0)
            {
                //guide base pair in the second quadrant, perpendicular is horizontal negative or in third quadrant
                perpendicularReference = new Vector(-1, 0);
                perpendicularQuadrant = TargetQuadrant.Third;
            }
            else
            {
                //this is the case where the guide base pair vector has a magnitude of 0
            }

            if (Math.Abs(Vector.AngleBetween(bpVector, perpendicularReference)) == 90)
            {
                StackToNeighboringBasePair(bpPadding, bpVector, perpendicularReference, t_nextFivePrimeStdCoord, t_nextThreePrimeStdCoord, guideBpFivePrimeIdx + 1, guideBpThreePrimeIdx - 1, deltas); 
            }
            else
            {
                Vector bpVectorPerpendicularUnitVector = CalculatePerpendicularUnitVector(bpVector, perpendicularQuadrant);
                StackToNeighboringBasePair(bpPadding, bpVector, bpVectorPerpendicularUnitVector, t_nextFivePrimeStdCoord, t_nextThreePrimeStdCoord, guideBpFivePrimeIdx + 1, guideBpThreePrimeIdx - 1, deltas);
            }  
        }

        private Vector CalculatePerpendicularUnitVector(Vector bpVector, TargetQuadrant target)
        {
            double bpVectorSlope = bpVector.Y / bpVector.X;
            double targetSlope = -1 / bpVectorSlope;
            double targetUnitVectorX = Math.Sqrt(1 / (1 + (Math.Pow(targetSlope, 2))));
            double targetUnitVectorY = targetSlope * targetUnitVectorX;

            switch (target)
            {
                case TargetQuadrant.Second:
                    targetUnitVectorY = (targetUnitVectorY < 0) ? -1 * targetUnitVectorY : targetUnitVectorY;
                    targetUnitVectorX = (targetUnitVectorX > 0) ? -1 * targetUnitVectorX : targetUnitVectorX;
                    break;
                case TargetQuadrant.Third:
                    targetUnitVectorX = (targetUnitVectorX > 0) ? -1 * targetUnitVectorX : targetUnitVectorX;
                    targetUnitVectorY = (targetUnitVectorY > 0) ? -1 * targetUnitVectorY : targetUnitVectorY;
                    break;
                case TargetQuadrant.Fourth:
                    targetUnitVectorX = (targetUnitVectorX < 0) ? -1 * targetUnitVectorX : targetUnitVectorX;
                    targetUnitVectorY = (targetUnitVectorY > 0) ? -1 * targetUnitVectorY : targetUnitVectorY;
                    break;
                case TargetQuadrant.First:
                    targetUnitVectorX = (targetUnitVectorX < 0) ? -1 * targetUnitVectorX : targetUnitVectorX;
                    targetUnitVectorY = (targetUnitVectorY < 0) ? -1 * targetUnitVectorY : targetUnitVectorY;
                    break;
                default:
                    break;
            };

            return new Vector(targetUnitVectorX, targetUnitVectorY);
        }

        private void StackToNeighboringBasePair(double separationLength, Vector bpVector, Vector bpVectorPerpendicularUnitVector, Point fivePrimeTargetCurrentLocationStdCoord, Point threePrimeTargetCurrentLocationStdCoord,
            int targetFivePrimeIdx, int targetThreePrimeIdx, Dictionary<int, Point> deltas)
        {
            Vector targetFivePrime = Vector.Multiply(separationLength, bpVectorPerpendicularUnitVector);
            Point deltaFivePrime = new Point(targetFivePrime.X - fivePrimeTargetCurrentLocationStdCoord.X, -1 * (targetFivePrime.Y - fivePrimeTargetCurrentLocationStdCoord.Y));
            Point threePrimeLocation = Vector.Add(bpVector, new Point(targetFivePrime.X, targetFivePrime.Y));
            Point deltaThreePrime = new Point(threePrimeLocation.X - threePrimeTargetCurrentLocationStdCoord.X, -1 * (threePrimeLocation.Y - threePrimeTargetCurrentLocationStdCoord.Y));
            AllLocations[targetFivePrimeIdx] = MoveNucleotideByInternal(targetFivePrimeIdx, deltaFivePrime);
            AllLocations[targetThreePrimeIdx] = MoveNucleotideByInternal(targetThreePrimeIdx, deltaThreePrime);
            deltas.Add(targetFivePrimeIdx, deltaFivePrime);
            deltas.Add(targetThreePrimeIdx, deltaThreePrime);
        }

        private enum TargetQuadrant { First, Second, Third, Fourth };
    }
}
